package org.java_websocket.client;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.NotYetConnectedException;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketAdapter;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.exceptions.InvalidHandshakeException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.framing.Framedata;
import org.java_websocket.framing.Framedata.Opcode;
import org.java_websocket.handshake.HandshakeImpl1Client;
import org.java_websocket.handshake.Handshakedata;
import org.java_websocket.handshake.ServerHandshake;
/**
* A subclass must implement at least <var>onOpen</var>, <var>onClose</var>, and
* <var>onMessage</var> to be useful. At runtime the user is expected to
* establish a connection via {@link #connect()}, then receive events like
* {@link #onMessage(String)} via the overloaded methods and to
* {@link #send(String)} data to the server.
*/
public abstract class WebSocketClient extends WebSocketAdapter implements
Runnable, WebSocket {
/**
* The URI this channel is supposed to connect to.
*/
protected URI uri = null;
private WebSocketImpl engine = null;
private Socket socket = null;
private InputStream istream;
private OutputStream ostream;
private Proxy proxy = Proxy.NO_PROXY;
private Thread writeThread;
private Draft draft;
private Map<String, String> headers;
private CountDownLatch connectLatch = new CountDownLatch(1);
private CountDownLatch closeLatch = new CountDownLatch(1);
private int connectTimeout = 0;
/** This open a websocket connection as specified by rfc6455 */
public WebSocketClient(URI serverURI) {
this(serverURI, new Draft_17());
}
/**
* Constructs a WebSocketClient instance and sets it to the connect to the
* specified URI. The channel does not attampt to connect automatically. The
* connection will be established once you call <var>connect</var>.
*/
public WebSocketClient(URI serverUri, Draft draft) {
this(serverUri, draft, null, 0);
}
public WebSocketClient(URI serverUri, Draft protocolDraft,
Map<String, String> httpHeaders, int connectTimeout) {
if (serverUri == null) {
throw new IllegalArgumentException();
} else if (protocolDraft == null) {
throw new IllegalArgumentException(
"null as draft is permitted for `WebSocketServer` only!");
}
this.uri = serverUri;
this.draft = protocolDraft;
this.headers = httpHeaders;
this.connectTimeout = connectTimeout;
this.engine = new WebSocketImpl(this, protocolDraft);
}
/**
* Returns the URI that this WebSocketClient is connected to.
*/
public URI getURI() {
return uri;
}
/**
* Returns the protocol version this channel uses.<br>
* For more infos see
* https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts
*/
public Draft getDraft() {
return draft;
}
/**
* Initiates the websocket connection. This method does not block.
*/
public void connect() {
if (writeThread != null)
throw new IllegalStateException(
"WebSocketClient objects are not reuseable");
writeThread = new Thread(this);
writeThread.start();
}
/**
* Same as <code>connect</code> but blocks until the websocket connected or
* failed to do so.<br>
* Returns whether it succeeded or not.
**/
public boolean connectBlocking() throws InterruptedException {
connect();
connectLatch.await();
return engine.isOpen();
}
/**
* Initiates the websocket close handshake. This method does not block<br>
* In oder to make sure the connection is closed use
* <code>closeBlocking</code>
*/
public void close() {
if (writeThread != null) {
engine.close(CloseFrame.NORMAL);
}
}
public void closeBlocking() throws InterruptedException {
close();
closeLatch.await();
}
/**
* Sends <var>text</var> to the connected websocket server.
*
* @param text
* The string which will be transmitted.
*/
public void send(String text) throws NotYetConnectedException {
engine.send(text);
}
/**
* Sends binary <var> data</var> to the connected webSocket server.
*
* @param data
* The byte-Array of data to send to the WebSocket server.
*/
public void send(byte[] data) throws NotYetConnectedException {
engine.send(data);
}
public void run() {
try {
if (socket == null) {
socket = new Socket(proxy);
} else if (socket.isClosed()) {
throw new IOException();
}
if (!socket.isBound())
socket.connect(new InetSocketAddress(uri.getHost(), getPort()),
connectTimeout);
istream = socket.getInputStream();
ostream = socket.getOutputStream();
sendHandshake();
} catch ( /*
* IOException | SecurityException | UnresolvedAddressException
* | InvalidHandshakeException | ClosedByInterruptException |
* SocketTimeoutException
*/Exception e) {
onWebsocketError(engine, e);
engine.closeConnection(CloseFrame.NEVER_CONNECTED, e.getMessage());
return;
}
writeThread = new Thread(new WebsocketWriteThread());
writeThread.start();
byte[] rawbuffer = new byte[WebSocketImpl.RCVBUF];
int readBytes;
try {
while (!isClosed() && (readBytes = istream.read(rawbuffer)) != -1) {
engine.decode(ByteBuffer.wrap(rawbuffer, 0, readBytes));
}
engine.eot();
} catch (IOException e) {
engine.eot();
} catch (RuntimeException e) {
// this catch case covers internal errors only and indicates a bug
// in this websocket implementation
onError(e);
engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
}
assert (socket.isClosed());
}
private int getPort() {
int port = uri.getPort();
if (port == -1) {
String scheme = uri.getScheme();
if (scheme.equals("wss")) {
return WebSocket.DEFAULT_WSS_PORT;
} else if (scheme.equals("ws")) {
return WebSocket.DEFAULT_PORT;
} else {
throw new RuntimeException("unkonow scheme" + scheme);
}
}
return port;
}
private void sendHandshake() throws InvalidHandshakeException {
String path;
String part1 = uri.getPath();
String part2 = uri.getQuery();
if (part1 == null || part1.length() == 0)
path = "/";
else
path = part1;
if (part2 != null)
path += "?" + part2;
int port = getPort();
String host = uri.getHost()
+ (port != WebSocket.DEFAULT_PORT ? ":" + port : "");
HandshakeImpl1Client handshake = new HandshakeImpl1Client();
handshake.setResourceDescriptor(path);
handshake.put("Host", host);
if (headers != null) {
for (Map.Entry<String, String> kv : headers.entrySet()) {
handshake.put(kv.getKey(), kv.getValue());
}
}
engine.startHandshake(handshake);
}
/**
* This represents the state of the connection.
*/
public READYSTATE getReadyState() {
return engine.getReadyState();
}
/**
* Calls subclass' implementation of <var>onMessage</var>.
*/
@Override
public final void onWebsocketMessage(WebSocket conn, String message) {
onMessage(message);
}
@Override
public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) {
onMessage(blob);
}
@Override
public void onWebsocketMessageFragment(WebSocket conn, Framedata frame) {
onFragment(frame);
}
/**
* Calls subclass' implementation of <var>onOpen</var>.
*/
@Override
public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) {
connectLatch.countDown();
onOpen((ServerHandshake) handshake);
}
/**
* Calls subclass' implementation of <var>onClose</var>.
*/
@Override
public final void onWebsocketClose(WebSocket conn, int code, String reason,
boolean remote) {
connectLatch.countDown();
closeLatch.countDown();
if (writeThread != null)
writeThread.interrupt();
try {
if (socket != null)
socket.close();
} catch (IOException e) {
onWebsocketError(this, e);
}
onClose(code, reason, remote);
}
/**
* Calls subclass' implementation of <var>onIOError</var>.
*/
@Override
public final void onWebsocketError(WebSocket conn, Exception ex) {
onError(ex);
}
@Override
public final void onWriteDemand(WebSocket conn) {
// nothing to do
}
@Override
public void onWebsocketCloseInitiated(WebSocket conn, int code,
String reason) {
onCloseInitiated(code, reason);
}
@Override
public void onWebsocketClosing(WebSocket conn, int code, String reason,
boolean remote) {
onClosing(code, reason, remote);
}
public void onCloseInitiated(int code, String reason) {
}
public void onClosing(int code, String reason, boolean remote) {
}
public WebSocket getConnection() {
return engine;
}
@Override
public InetSocketAddress getLocalSocketAddress(WebSocket conn) {
if (socket != null)
return (InetSocketAddress) socket.getLocalSocketAddress();
return null;
}
@Override
public InetSocketAddress getRemoteSocketAddress(WebSocket conn) {
if (socket != null)
return (InetSocketAddress) socket.getRemoteSocketAddress();
return null;
}
// ABTRACT METHODS /////////////////////////////////////////////////////////
public abstract void onOpen(ServerHandshake handshakedata);
public abstract void onMessage(String message);
public abstract void onClose(int code, String reason, boolean remote);
public abstract void onError(Exception ex);
public void onMessage(ByteBuffer bytes) {
}
public void onFragment(Framedata frame) {
}
private class WebsocketWriteThread implements Runnable {
@Override
public void run() {
Thread.currentThread().setName("WebsocketWriteThread");
try {
while (!Thread.interrupted()) {
ByteBuffer buffer = engine.outQueue.take();
ostream.write(buffer.array(), 0, buffer.limit());
ostream.flush();
}
} catch (IOException e) {
engine.eot();
} catch (InterruptedException e) {
// this thread is regularly terminated via an interrupt
}
}
}
public void setProxy(Proxy proxy) {
if (proxy == null) {
throw new IllegalArgumentException();
}
this.proxy = proxy;
}
/**
* Accepts bound and unbound sockets.<br>
* This method must be called before <code>connect</code>. If the given
* socket is not yet bound it will be bound to the uri specified in the
* constructor.
**/
public void setSocket(Socket socket) {
if (this.socket != null) {
throw new IllegalStateException("socket has already been set");
}
this.socket = socket;
}
@Override
public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) {
engine.sendFragmentedFrame(op, buffer, fin);
}
@Override
public boolean isOpen() {
return engine.isOpen();
}
@Override
public boolean isFlushAndClose() {
return engine.isFlushAndClose();
}
@Override
public boolean isClosed() {
return engine.isClosed();
}
@Override
public boolean isClosing() {
return engine.isClosing();
}
@Override
public boolean isConnecting() {
return engine.isConnecting();
}
@Override
public boolean hasBufferedData() {
return engine.hasBufferedData();
}
@Override
public void close(int code) {
engine.close();
}
@Override
public void close(int code, String message) {
engine.close(code, message);
}
@Override
public void closeConnection(int code, String message) {
engine.closeConnection(code, message);
}
@Override
public void send(ByteBuffer bytes) throws IllegalArgumentException,
NotYetConnectedException {
engine.send(bytes);
}
@Override
public void sendFrame(Framedata framedata) {
engine.sendFrame(framedata);
}
@Override
public InetSocketAddress getLocalSocketAddress() {
return engine.getLocalSocketAddress();
}
@Override
public InetSocketAddress getRemoteSocketAddress() {
return engine.getRemoteSocketAddress();
}
@Override
public String getResourceDescriptor() {
return uri.getPath();
}
}